package prictise.com.application1.cusListview;

import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.view.animation.Transformation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;

import java.text.DateFormat;
import java.util.Date;

import prictise.com.application1.R;


/**
 * @Author zsj
 * @Date 2018-11-11 21:42
 * @Commit
 */
public class RefreshListView extends ListView {
    private static final int RESISTANCE = 3;
    private static final int HEADER_HEIGHT = 60;
    private static final int DURATION = 300;

    private OnRefreshListener refreshListener;
    private View container;
    private RelativeLayout header;
    private ProgressBar progress;
    private TextView comment;
    private ImageView arrow;
    private TextView date;
    private LayoutInflater inflater;

    private float currentY;

    private int headerHeight;

    private boolean enabledDate;
    private Date lastUpdateDate;
    private DateFormat formatter;

    private State currentState;

    private enum State {
        PULLDOWN, RELEASE, UPDATING;
    }

    private enum Rotation {
        CLOCKWISE, ANTICLOCKWISE;
    }

    public RefreshListView(Context context) {
        super(context);
        init(context);
    }

    public RefreshListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public RefreshListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    /**
     * initializing method. Call in constructors to set up the headers.
     *
     * @param context activity context, got by constructors
     */
    private void init(Context context) {
        formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT);
        inflater = LayoutInflater.from(context);
        container = inflater.inflate(R.layout.layout_refreshlistview_header, null);
        header = (RelativeLayout) container.findViewById(R.id.header);
        arrow = (ImageView) container.findViewById(R.id.arrow);
        progress = (ProgressBar) container.findViewById(R.id.progress);
        date = (TextView) container.findViewById(R.id.date);
        comment = (TextView) container.findViewById(R.id.comment);

        container.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        header.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));

        addHeaderView(container);

        headerHeight = (int) (HEADER_HEIGHT * getContext().getResources().getDisplayMetrics().density);
        changeHeaderHeight(0);

        comment.setText(getResources().getString(R.string.refreshlistview_pulldown));
        currentState = State.PULLDOWN;
    }

    /**
     * Call to perform item click. Reset the position without the header
     *
     * @see android.widget.AbsListView#performItemClick(android.view.View, int, long)
     */
    @Override
    public boolean performItemClick(View view, int position, long id) {
        if (position == 0) {
            return true;
        } else {
            return super.performItemClick(view, position - getHeaderViewsCount(), id);
        }
    }

    /**
     * Set up first touch to later calculations.
     *
     * @see android.widget.AbsListView#onInterceptTouchEvent(android.view.MotionEvent)
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                currentY = ev.getY();
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    /**
     * Handle what to do when the user release the touch.
     *
     * @see android.widget.AbsListView#onTouchEvent(android.view.MotionEvent)
     */
    @Override
    public boolean onTouchEvent(final MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_UP:
                if (currentState != State.UPDATING) {
                    if (currentState == State.RELEASE) {
                        header.startAnimation(new ResizeHeaderAnimation(headerHeight));
                        startRefreshing();
                    } else {
                        header.startAnimation(new ResizeHeaderAnimation(0));
                    }
                } else {
                    header.startAnimation(new ResizeHeaderAnimation(headerHeight));
                }
        }
        return super.onTouchEvent(ev);
    }

    /**
     * Used to show header when scrolling down.
     *
     * @see android.view.ViewGroup#dispatchTouchEvent(android.view.MotionEvent)
     */
    @Override
    public boolean dispatchTouchEvent(final MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_MOVE && getFirstVisiblePosition() == 0) {
            if (isAllowedToShowHeader(ev.getY())) {
                changeHeaderHeight(getHeightWithScrollResistance(ev.getY()));
            }
        }
        return super.dispatchTouchEvent(ev);
    }

    /**
     * Call to update UI when the refresh started.
     */
    private void startRefreshing() {
        arrow.clearAnimation();
        arrow.setVisibility(View.INVISIBLE);
        progress.setVisibility(View.VISIBLE);
        comment.setText(getResources().getString(R.string.refreshlistview_updating));

        if (refreshListener != null) {
            refreshListener.onRefresh(this);
        }

        currentState = State.UPDATING;
    }

    /**
     * Call when refreshing task is done. Must be called by the developer.
     */
    public void finishRefreshing() {
        finishRefreshing(null);
    }

    /**
     * Call when refreshing task is done. Must be called by the developer.
     *
     * @param updateDate allow developer to set the last updateDate
     */
    public void finishRefreshing(Date updateDate) {
        header.startAnimation(new ResizeHeaderAnimation(0));
        progress.setVisibility(View.INVISIBLE);
        arrow.setVisibility(View.VISIBLE);
        if (updateDate == null)
            lastUpdateDate = new Date();
        else
            lastUpdateDate = updateDate;
        date.setText(getFormattedDate(lastUpdateDate));
        comment.setText(getResources().getString(R.string.refreshlistview_pulldown));
        currentState = State.PULLDOWN;
        invalidate();
    }

    /**
     * Change the header height while scrolling down by making it visible and increasing topMargin of the header.
     *
     * @param height the height of the header
     */
    private void changeHeaderHeight(int height) {
        hideOrShowHeader(height);

        LayoutParams layoutParams = (LayoutParams) container.getLayoutParams();
        layoutParams.height = height;
        container.setLayoutParams(layoutParams);

        LinearLayout.LayoutParams headerLayoutParams = (LinearLayout.LayoutParams) header.getLayoutParams();
        headerLayoutParams.topMargin = height - headerHeight;
        header.setLayoutParams(headerLayoutParams);

        if (currentState != State.UPDATING) {
            if (height > headerHeight && currentState == State.PULLDOWN) {
                arrow.startAnimation(getRotationAnimation(Rotation.ANTICLOCKWISE));
                comment.setText(getResources().getString(R.string.refreshlistview_release));
                currentState = State.RELEASE;
            } else if (height < headerHeight && currentState == State.RELEASE) {
                arrow.startAnimation(getRotationAnimation(Rotation.CLOCKWISE));
                comment.setText(getResources().getString(R.string.refreshlistview_pulldown));
                currentState = State.PULLDOWN;
            }
        }
    }

    /**
     * Check whether or not the header should be shown.
     *
     * @param newY just acquired Y event
     * @return true if it is, false otherwise
     */
    private boolean isAllowedToShowHeader(float newY) {
        return isScrollingEnough(newY) && (currentState != State.UPDATING || (currentState == State.UPDATING && (newY - currentY) > 0));
    }

    /**
     * Check if the scroll is enough to be taken into acccount.
     *
     * @param newY just acquired Y event
     * @return true if it is, false otherwise
     */
    private boolean isScrollingEnough(float newY) {
        float deltaY = Math.abs(currentY - newY);
        ViewConfiguration config = ViewConfiguration.get(getContext());
        return deltaY > config.getScaledTouchSlop();
    }

    /**
     * Calculate height header when scrolling down.
     *
     * @param newY just acquired Y event
     * @return the height of the header to set
     */
    private int getHeightWithScrollResistance(float newY) {
        return Math.max((int) (newY - currentY) / RESISTANCE, 0);
    }

    /**
     * Hide or show the header according to its height.
     *
     * @param height current height of the header
     */
    private void hideOrShowHeader(int height) {
        if (height <= 0) {
            header.setVisibility(View.GONE);
        } else {
            header.setVisibility(View.VISIBLE);
        }
    }

    /**
     * Getter to know if date is enabled
     *
     * @return true if date is enabled, false otherwise
     */
    public boolean isEnabledDate() {
        return enabledDate;
    }

    /**
     * Set enabled date, the first date to show will just be "No past update".
     *
     * @param enabledDate
     */
    public void setEnabledDate(boolean enabledDate) {
        setEnabledDate(enabledDate, null);
    }

    public void setEnabledDate(boolean enabledDate, Date firstDate) {
        this.enabledDate = enabledDate;
        lastUpdateDate = firstDate;
        if (enabledDate) {
            date.setVisibility(View.VISIBLE);
            if (firstDate != null) {
                date.setText(getFormattedDate(firstDate));
            } else {
                date.setText(getResources().getString(R.string.refreshlistview_no_update));
            }
        } else {
            date.setVisibility(View.GONE);
        }
    }

    /**
     * Getter for last update date.
     *
     * @return the last update date or null is it has never been updated yet.
     */
    public Date getLastUpdateDate() {
        return lastUpdateDate;
    }

    private String getFormattedDate(Date date) {
        return formatter.format(date);
    }

    public void setRefreshListener(OnRefreshListener listener) {
        this.refreshListener = listener;
    }

    /**
     * Callback. Call when user asks to refresh the list. Required to be implemented by developer.
     */
    public interface OnRefreshListener {
        public void onRefresh(RefreshListView listView);
    }

    private Animation getRotationAnimation(Rotation rotation) {
        int fromAngle = 0;
        int toAngle = 0;
        if (rotation == Rotation.ANTICLOCKWISE)
            toAngle = 180;
        else
            fromAngle = 180;
        Animation animation = new RotateAnimation(fromAngle, toAngle, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
        animation.setDuration(DURATION);
        animation.setFillAfter(true);
        return animation;
    }

    /**
     * Animation to resize the header's height
     */
    public class ResizeHeaderAnimation extends Animation {
        private int toHeight;

        public ResizeHeaderAnimation(int toHeight) {
            this.toHeight = toHeight;
            setDuration(DURATION);
        }

        /**
         * Animation core, animate the height of the header to a specific value.
         *
         * @see android.view.animation.Animation#applyTransformation(float, android.view.animation.Transformation)
         */
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            float height = (toHeight - container.getHeight()) * interpolatedTime + container.getHeight();
            LayoutParams lp = (LayoutParams) container.getLayoutParams();
            LinearLayout.LayoutParams headerlp = (LinearLayout.LayoutParams) header.getLayoutParams();
            headerlp.topMargin = (int) height - headerHeight;
            lp.height = (int) height;
            lp.width = (int) container.getWidth();
            container.requestLayout();
        }

        /**
         * Used at the end of the animation to hide completely the header if it's required (toHeight == 0).
         *
         * @see android.view.animation.Animation#getTransformation(long, android.view.animation.Transformation)
         */
        @Override
        public boolean getTransformation(long currentTime, Transformation outTransformation) {
            hideOrShowHeader(toHeight);
            return super.getTransformation(currentTime, outTransformation);
        }
    }

    /**
     * Display a toast when there is an error in refresh task
     *
     * @param errorMessage error message to display
     */
    public void errorInRefresh(String errorMessage) {
        Toast.makeText(getContext(), errorMessage, Toast.LENGTH_SHORT).show();
    }
}
